Skip to main content
  1. SwiftUI in 100 Days Notes/

Day 15 - A Quick Overview of Swift Basics

In this article, we’re going to do a quick review of the basics of Swift that we’ve been looking at for the past 14 days. In this article, we will summarize the basics of Swift without going deeper into the subject.

Creating Constants and Variables #

Constants and variables can be created in Swift, but constants are generally preferred.

I want to create a String variable and then modify it;

var name = "Ted"
name = "Rebecca"

If we don’t want to change a value in the future, use a constant instead;

let user = "Daphne"

The print() function is useful for learning and debugging. It shows the value of the constant user;

print(user)

String #

In Swift, strings are between double quotes " ";

let actor = "Tom Cruise"

We can also use emojis;

let actor = "Tom Cruise 🏃‍♂️"

If we want to use " double quotes in a string, we put a backslash \ in front of them;

let quote = "He tapped a sign saying \"Believe\" and walked away."

If we want to use a multiline string, we must start and end with three double quotes as follows """;

let movie = """
A day in
the life of an
Apple engineer
"""

Swift provides many useful methods for strings. With the .count method we can find out how many characters the string has;

print(actor.count)

There are also hasPrefix() and hasSuffix() methods that let us know if the string starts or ends with certain letters.

print(quote.hasPrefix("He"))
print(quote.hasSuffix("Away."))

**Important: Strings are case sensitive in Swift, so the second check will return false.

Integer #

Swift stores integers using the Int type, which supports standard arithmetic operators;

let score = 10
let higherScore = score + 10
let halvedScore = score / 2

It also supports compound assignment operators that modify variables in place;

var counter = 10
counter += 5

Integers have their own useful methods, such as the isMultiple(of:) method.

let number = 120
print(number.isMultiple(of: 3))

Like this, we can generate random integers in a certain range;

let id = Int.random(in: 1...1000)

Decimal #

If we create a decimal number, Swift will recognize it as a Double.

let score = 3.0

Swift considers Double to be a completely different data type from Int and does not allow us to mix them.

Boolean #

Swift uses the Bool type to store true and false;

let goodDogs = true
let gameOver = false

We can change a Boolean from true to false by calling the toggle() method.

var isSaved = false
isSaved.toggle()

String Interpolation #

Using String Interpolation we can create strings from other data, like this one;

let name = "Taylor"
let age = 26
let message = "I'm \(name) and I'm \(age) years old."
print(message)

//OUTPUT : I’m Taylor and I’m 26 years old.

ArrayS #

We can group items into an Array;

var colors = ["Red", "Green", "Blue"]
let numbers = [4, 8, 15, 16]
var readings = [0.1, 0.5, 0.8]

Each of these arrays holds different types of data: colors String, number integers, readings decimal numbers.

print(colors[0])
//Red
print(readings[2])
//0.8

Tip: When reading data by index number, make sure that there is an item in that index, otherwise our code will crash and our application will stop working.

If Array is a variable (var), we can use the append() method to add new items.

colors.append("Tartan")

There are useful functions like .count to find out how many items are in the Array, and remove(at:) to delete an item at a given index.

colors.remove(at: 0)
print(colors.count)
//2

We can check whether an Array contains a specific element with the contains() method.

print(colors.contains("Octarine"))
//false

Dictionary #

Dictionary stores multiple values according to a key we specify.

let employee = [
    "name": "Taylor",
    "job": "Singer"
]

To read data from the dictionary, we use the same key we used to create the dictionary;

print(employee["name", default: "Unknown"])
print(employee["job", default: "Unknown"])

If the key we want is not available, the default value will be used.

Set #

Sets are similar to Array except that we cannot add duplicate items and they do not store items in a specific order.

var numbers = Set([1, 1, 3, 5, 7])
print(numbers)
//[1, 3, 7, 5]

Note that Sets will ignore duplicate values and will not remember the order used in Array.

Adding items to a Set is done with the insert() method.

numbers.insert(10)

The biggest advantage of Sets over Array is that they are very fast. A Set of 10,000,000 items will respond instantly even when we search with the contains() method.

Enum #

An enum is a set of named values that we can create and use to make our code more efficient and more secure. For example, we can create an enum for weekdays.

enum Weekday {
    case monday, tuesday, wednesday, thursday, friday
}

Calls the new enum Weekday and provides five states to handle the five days of the week.

var day = Weekday.monday
day = .friday

Type Annotation #

Using Type annotation, we can specify the type of a variable or constant when it is created.

var score: Double = 0

Without the Double part in the code above, Swift would conclude that it is an Int, but we override that and say that it is a Double.

Here are some examples of type annotations;

let player: String = "Roy"
var luckyNumber: Int = 13
let pi: Double = 3.141
var isEnabled: Bool = true
var albums: Array<String> = ["Red", "Fearless"]
var user: Dictionary<String, String> = ["id": "@twostraws"]
var books: Set<String> = Set(["The Bluest Eye", "Foundation"])

The use of Array and Dictionary is so common that they have special syntax that is easier to write.

var albums: [String] = ["Red", "Fearless"]
var user: [String: String] = ["id": "@twostraws"]

To create empty collections, it is important to know the exact types of the objects. For example, both of these create empty string arrays.

var teams: [String] = [String]()
var clues = [String]()

The values of an enum have the same type as the enum itself, so we can write this;

enum UIStyle {
    case light, dark, system
}

var style: UIStyle = .light

Conditions #

The if, else and else if statements are used to check a condition and execute some code accordingly.

let age = 16

if age < 12 {
    print("You can't vote")
} else if age < 18 {
    print("You can vote soon.")
} else {
    print("You can vote now.")
}

We can use && to combine two conditions, and the whole condition is true only if the two parts are true.

let temp = 26

if temp > 20 && temp < 30 {
    print("It's a nice day.")
}

Alternatively || means or. Makes the condition true if at least one of the subconditions is true.

Switch Statement (Switch Case) #

switch / case allows us to check a value against multiple conditions.

enum Weather {
    case sun, rain, wind
}

let forecast = Weather.sun

switch forecast {
case .sun:
    print("A nice day.")
case .rain:
    print("Pack an umbrella.")
default:
    print("Should be okay.")
}

switch expressions must be comprehensive. All possible values must be covered.

Ternary Operator #

The ternary operator allows us to check a condition and return one of two values: one thing if the condition is true, and one thing if the condition is false.

let age = 18
let canVote = age >= 18 ? "Yes" : "No"

When the code runs, the canVote constant will be set to “Yes” because age is set to 18.

Loops #

Swift’s for loops execute some code for each item in a collection or a custom range.

let platforms = ["iOS", "macOS", "tvOS", "watchOS"]

for os in platforms {
    print("Swift works on \(os).")
}

We can also loop over a range of numbers;

for i in 1...12 {
    print("5 x \(i) is \(5 * i)")
}

1...12 contains the numbers in the range, including 1 and 12. If we want to exclude the last number, we must use ..<.

for i in 1..<13 {
    print("5 x \(i) is \(5 * i)")
}

Tip: If we don’t need a loop variable, we can use _.

var lyric = "Haters gonna"

for _ in 1...5 {
    lyric += " hate"
}

print(lyric)

There are also while loops that execute the loop body until a condition is false.

var count = 10

while count > 0 {
    print("\(count)…")
    count -= 1
}

print("Go!")

Use continue to skip the current loop iteration and move to the next one.

let files = ["me.jpg", "work.txt", "sophie.jpg"]

for file in files {
    if file.hasSuffix(".jpg") == false {
        continue
    }

    print("Found picture: \(file)")
}

Alternatively, break is used to exit a loop and skip all remaining iterations.

Functions #

To create a new function, type func followed by the name of our function, followed by its parameters in parentheses.

func printTimesTables(number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(number: 5)

we use number: 5 when calling a function. Because the parameter name is also part of the function call.

To return data from a function, we need to say what type of data is being returned. To return the value inside the function, we must use the return keyword.

func rollDice() -> Int {
    return Int.random(in: 1...6)
}

let result = rollDice()
print(result)

Returning Multiple Values from a Function #

Tuples store a fixed number of values of a certain type, which is a convenient way to return multiple values from a function:

func getUser() -> (firstName: String, lastName: String) {
    (firstName: "Taylor", lastName: "Swift")
}

let user = getUser()
print("Name: \(user.firstName) \(user.lastName)")

If we don’t need all the values in the tuple, we can split the tuple and assign them, or use _ to ignore data in the tuple.

let (firstName, _) = getUser()
print("Name: \(firstName)")

Customization of Parameter Names #

If we don’t want to specify the name of a parameter when calling a function, we replace it with _.

func isUppercase(_ string: String) -> Bool {
    string == string.uppercased()
}

let string = "HELLO, WORLD"
let result = isUppercase(string)

Another alternative is to write a second name in front of the first name. The first is the external parameter name and the second is the internal parameter name.

func printTimesTables(for number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(for: 5)

In the code above, for is the external parameter name and number is the internal parameter name.

Providing Default Values for Parameters #

We can provide the default parameter value by assigning a value by typing equals after the type.

func greet(_ person: String, formal: Bool = false) {
    if formal {
        print("Welcome, \(person)!")
    } else {
        print("Hi, \(person)!")
    }
}

Now we can call greet() in two different ways.

greet("Tim", formal: true)
greet("Taylor")

Handling Errors in Functions #

In order to handle errors in functions, we need to tell Swift what errors can happen. We need to write a function that can throw errors, then call that function and handle the errors.

func checkPassword(_ password: String) throws -> String {
    if password.count < 5 {
        throw PasswordError.short
    }

    if password == "12345" {
        throw PasswordError.obvious
    }

    if password.count < 10 {
        return "OK"
    } else {
        return "Good"
    }
}

Now let’s start the do block and call the error-causing function using try and then catch the errors that occur

let string = "12345"

do {
    let result = try checkPassword(string)
    print("Rating: \(result)")
} catch PasswordError.obvious {
    print("I have the same combination on my luggage!")
} catch {
    print("There was an error.")
}

When it comes to catching errors, we should always have a catch block that can handle any type of error.

Closures #

We can assign the function directly to a constant or variable

let sayHello = {
    print("Hi there!")
}

sayHello()

In this code sayHello is a closure. If we want the closure to take parameters, these parameters should be written inside parentheses.

let sayHello = { (name: String) -> String in
    "Hi \(name)!"
}

in is used to mark the end of the parameter and return type. Everything after in is the body of the closure.

Closures are widely used in Swift. For example, there is a method called filter() that passes a test on all elements of an Array. Those that pass this test, i.e. true, are returned in a new Array.

We can provide this test using a closure, so that we can filter an Array to contain only names starting with T.

let team = ["Gloria", "Suzanne", "Tiffany", "Tasha"]

let onlyT = team.filter({ (name: String) -> Bool in
    return name.hasPrefix("T")
})

Inside the closure we list the parameter that the filter() function passes us, this parameter is a string from an array. We also say that our closure returns a Boolean, then we mark the beginning of the closure code using in, after that everything is normal function code.

Closure Short Syntax #

Swift has some solutions to make clousers easier to read. Let’s write again the code here where we filtered the array elements starting with T;

let team = ["Gloria", "Suzanne", "Tiffany", "Tasha"]

let onlyT = team.filter({ (name: String) -> Bool in
    return name.hasPrefix("T")
})

print(onlyT)

We can immediately see that there is only a single line of code in the body of the closure, so we can remove return.

let onlyT = team.filter({ (name: String) -> Bool in
    name.hasPrefix("T")
})

The filter() function should be given such a function as a parameter so that the function accepts a string from Array and returns boolean according to the condition.

We don’t need to specify the types in the closure, since the function we are importing should behave this way. Therefore we can write the code as follows;

let onlyT = team.filter { name in
    name.hasPrefix("T")
}

Finally, Swift can provide short parameter names for us so that we no longer even need to type name and in, but instead use the specially named parameters provided for us. Like $0.

let onlyT = team.filter {
    $0.hasPrefix("T")
}

Struct #

Struct allows us to create custom data types with their own properties and methods.

struct Album {
    let title: String
    let artist: String
    var isReleased = true

    func printSummary() {
        print("\(title) by \(artist)")
    }
}

let red = Album(title: "Red", artist: "Taylor Swift")
print(red.title)
red.printSummary()

When creating instances of structs, we do this by using an initializer. Swift allows us to treat the struct like a function and pass in parameters for each property. The memberwise initializer is automatically generated according to the properties of the struct.

If a struct’s method modifies one of its properties, this method should be marked as mutating.

mutating func removeFromSale() {
    isReleased = false
}

Computed Property #

The value of the calculated property is calculated each time it is accessed. For example, we can write an Employee struct that keeps track of how many days of vacation an employee has left;

struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0

    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}

To write to vacationRemaining we need to provide both getter and setter.

var vacationRemaining: Int {
    get {
        vacationAllocated - vacationTaken
    }

    set {
        vacationAllocated = vacationTaken + newValue
    }
}

newValue is provided by Swift and stores the value that the user assigns to the property.

Property Observer #

Property observer are pieces of code that run when the property changes. didSet runs after the property changes, willSet runs just before the property changes.

We can show didSet by creating a Game struct that prints a message when the score changes.

struct Game {
    var score = 0 {
        didSet {
            print("Score is now \(score)")
        }
    }
}

var game = Game()
game.score += 10
game.score -= 3

Custom Initializer #

Initializers are special functions that prepare a new struct instance and ensure that all its properties have an initial value.

Swift creates one based on the Struct’s properties, but we can also create a custom initializer if we want.

struct Player {
    let name: String
    let number: Int

    init(name: String) {
        self.name = name
        number = Int.random(in: 1...99)
    }
}

Important : Initializers are not preceded by func and do not explicitly return values.

Access Control #

Swift has several options for access control within structs, the four most commonly used are the following;

  • private : “Don’t allow anything other than Struct to use it”
  • private(set) : “Anything outside of Struct can read this, but don’t let them change it”
  • fileprivate : “Don’t allow anything but the current file to use it”
  • public: “Allow anyone, anywhere to use it.”

For example;

struct BankAccount {
    private(set) var funds = 0

    mutating func deposit(amount: Int) {
        funds += amount
    }

    mutating func withdraw(amount: Int) -> Bool {
        if funds > amount {
            funds -= amount
            return true
        } else {
            return false
        }
    }
}

Since we set the funds property to private(set), we can read this property from outside the Struct, but we cannot write it.

Static Propety and Method #

Swift allows us to add static properties and methods directly to the structure itself instead of to an instance of it.

struct AppData {
    static let version = "1.3 beta 2"
    static let settings = "settings.json"
}

Using this approach, we can read AppData.version wherever we need to check or display something like the version number of the application.

Class #

Classes allow us to create custom data types and are different from Structs in five ways;

  1. We can create classes by inheriting functionality from other classes.

    class Employee {
        let hours: Int
    
        init(hours: Int) {
            self.hours = hours
        }
    
        func printSummary() {
            print("I work \(hours) hours a day.")
        }
    }
    
    class Developer: Employee {
        func work() {
            print("I'm coding for \(hours) hours.")
        }
    }
    
    let novall = Developer(hours: 8)
    novall.work()
    novall.printSummary()
    

    A child class must use override if it wants to change the method it inherits from the parent class.

    override func printSummary() {
        print("I spend \(hours) hours a day searching Stack Overflow.")
    }
    
  2. The Initializer in classes can be a bit more complex. It has three important points;

    1. Swift does not create memberwise initializers for classes.
    2. If a child class has a custom initializer, it should always call the initializer of the parent class after it has finished setting its own properties.
    3. If a child class has no initializer, it automatically inherits the initializer of its parent class.

    For example;

    class Vehicle {
        let isElectric: Bool
    
        init(isElectric: Bool) {
            self.isElectric = isElectric
        }
    }
    
    class Car: Vehicle {
        let isConvertible: Bool
    
        init(isElectric: Bool, isConvertible: Bool) {
            self.isConvertible = isConvertible
            super.init(isElectric: isElectric)
        }
    }
    

    super allows us to call methods of our parent class, such as initializer.

  3. All copies of an instance of a class share data. So any changes we make to one will automatically change the other copies.

    For example;

    class Singer {
        var name = "Adele"
    }
    
    var singer1 = Singer()
    var singer2 = singer1
    singer2.name = "Justin"
    print(singer1.name)
    //Justin  
    print(singer2.name)
    //Justin
    

    Both prints will write Justin to the screen, even if we have only changed one of them and the other one has also changed. In contrast, copies of Struct instances do not share their data.

  4. Classes have a deinitializer that is called when the last reference to an object is destroyed.

    Thus, we can create a class that prints a message when created and destroyed;v

    class User {
        let id: Int
    
        init(id: Int) {
            self.id = id
            print("User \(id): I'm alive!")
        }
    
        deinit {
            print("User \(id): I'm dead!")
        }
    }
    
    for i in 1...3 {
        let user = User(id: i)
        print("User \(user.id): I'm in control!")
    }
    
  5. We can change variable (var) properties even if the instance of the class itself is a constant (let).

    class User {
        var name = "Paul"
    }
    
    let user = User()
    user.name = "Taylor"
    print(user.name)
    //Taylor
    

    As a consequence, classes do not need the mutating keyword in methods that modify their data.

Protocol #

Protocols define the functionality we expect a data type to support, and Swift ensures that our code follows these rules.

For example, let’s define a Vehicle protocol below.

protocol Vehicle {
    func estimateTime(for distance: Int) -> Int
    func travel(distance: Int)
}

This specifies the methods required for the protocol to work, but does not contain any code, we only specify the method names, parameters and return types.

Once we have a protocol, we can ensure that the data types conform to it by implementing the necessary functionality. For example, we can create a Car struct compatible with Vehicle.

struct Car: Vehicle {
    func estimateTime(for distance: Int) -> Int {
        distance / 50
    }

    func travel(distance: Int) {
        print("I'm driving \(distance)km.")
    }
}

All methods listed in Vehicle must exist exactly in Car with the same name, parameters and return types.

Now we can write a function that accepts any type that fits Vehicle, because Swift knows that both estimateTime() and travel() apply.

func commute(distance: Int, using vehicle: Vehicle) {
    if vehicle.estimateTime(for: distance) > 100 {
        print("Too slow!")
    } else {
        vehicle.travel(distance: distance)
    }
}

let car = Car()
commute(distance: 100, using: car)

Protocols can also include properties. Let’s add to our Vehicle protocol;

protocol Vehicle {
    var name: String { get }
    var currentPassengers: Int { get set }
    func estimateTime(for distance: Int) -> Int
    func travel(distance: Int)
}

This adds two properties. The one marked with get can be a constant or a computed property. A variable marked with get set or a property computed with getter and setter.

All types that conform to Vehicle must implement these two properties.

let name = "Car"
var currentPassengers = 1

**Hint: We can follow as many protocols as we want, separated by commas.

Extension #

Extension allows us to add functionality to any type. For example, Swift has a method for deleting spaces from strings, we can rewrite the name of this method to be shorter with extension.

extension String {
    func trimmed() -> String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

var quote = "   The truth is rarely pure and never simple   "
let trimmed = quote.trimmed()

If we want to modify a value directly instead of returning a new value, we should mark our method as mutating like this.

extension String {
    mutating func trim() {
        self = self.trimmed()
    }
}

quote.trim()

Extension can also add computed properties like this to types.

extension String {
    var lines: [String] {
        self.components(separatedBy: .newlines)
    }
}

The components(separatedBy:) method can split a string according to certain criteria.

Now we can use the lines property with all strings.

let lyrics = """
But I keep cruising
Can't stop, won't stop moving
"""

print(lyrics.lines.count)

Protocol Extension #

Protocol extension extends the entire protocol to add computed property and method implementations. This way all types that conform to the protocol can use computed properties and methods.

Array, Dictionary and Set conform to the Collection protocol. For this reason, we can add computed property to all three as follows;

extension Collection {
    var isNotEmpty: Bool {
        isEmpty == false
    }
}

Now we can use this;

let guests = ["Mario", "Luigi", "Peach"]

if guests.isNotEmpty {
    print("Guest count: \(guests.count)")
}

This approach means that we can list the required methods in a protocol and then add their default implementations in a protocol extension. All types implementing this protocol can then use these default implementations or provide their own implementations as needed.

Optionals #

Optional represents the absence of data. For example, they distinguish between an integer with a value of 0 and an integer with no value.

We can examine how Options work;

let opposites = [
    "Mario": "Wario",
    "Luigi": "Waluigi"
]

let peachOpposite = opposites["Peach"]

This tries to read the value bound to the non-existent key “Peach”, so we can’t use a normal string. Swift’s solution is called optional. Optional means data that may or may not exist.

An optional string can have a string waiting for us in it, or it can have nothing (a special value called nil, which means “no value”). Any data type can be optional, including Int, Double, Bool as well as enum, struct and class instances.

We cannot use Optional data directly because there is a possibility that it is empty. To use Optional we need to unwrap it.

We have several options to unwrap the Optional, but the most used method is the following;

if let marioOpposite = opposites["Mario"] {
    print("Mario's opposite is \(marioOpposite)")
}

The code above reads the optional value from the dictionary and unwraps it if it contains a string, the unwrapped string is placed in the marioOpposite constant, it is no longer optional. Since we can unwrap the optional value, the condition succeeds and the print() code is executed.

Using guard for Unwrapping of Optionals #

Swift has a second way to unwrap optional’s called guard let. This method is very similar to if let but reverses things. if let executes the code in parentheses if optional has a value. guard let executes the code if it does not have an optional value.

It looks like this;

func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")
        return
    }

    print("\(number) x \(number) is \(number * number)")
}

We use guard to check if the inputs of a function are valid, and we must use return in case the check fails. However, if there is a value in the optional that we unwrap, we can use it after the guard code is finished.

Tip: guard can be used with any condition.

Nil Coalescing #

A third way of unwrapping is called nil coalescing. This method unwraps an optional, and if the optional is empty, a default value is provided.

let tvShows = ["Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"

The nil coalescing operator is useful in many places where optional’s are created. For example, when converting a string to int, it makes sense to use nil coalescing instead of creating an optional Int? in case the conversion fails.

let input = ""
let number = Int(input) ?? 0
print(number)

Optional Chaining #

Optional chaining reads optional from optional. Like this

let names = ["Arya", "Bran", "Robb", "Sansa"]
let chosen = names.randomElement()?.uppercased()
print("Next in line: \(chosen ?? "No one")")

Optional chaining is on line 2. It allows us to say “if the option has a value in it, open it and then ….” and add more code. In our case we say “if we get a random element from Array, capitalize it”.

Optional try? #

When calling a throwing function, we can use the try? option to convert the result of the function to an optional with a value on success, or nil otherwise.

enum UserError: Error {
    case badID, networkFailed
}

func getUser(id: Int) throws -> String {
    throw UserError.networkFailed
}

if let user = try? getUser(id: 23) {
    print("User: \(user)")
}

The getUser() function will always throw a networkFailed error, but what is thrown is not important to us. What matters to us is whether the call to the function sends us back a user.


You can also read this article in Turkish.
Bu yazıyı Türkçe olarak da okuyabilirsiniz.

This article contains the notes I took for myself from the articles found at SwiftUI Day 15. Please use the link to follow the original lesson.